spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) - #18985
Conversation
… fault semantics `PredicateSchema` / `PredicateInputSchema` composed the PERSISTENCE contract (`source` OR `ast`) while a predicate exists to be evaluated and the CEL engine reads `source` alone. An `ast`-only envelope and a `source` blank after trimming parsed, registered, passed `objectstack validate`, then faulted or short-circuited at evaluation time. They now compose the evaluated rule, and the field-rule triad on `FieldSchema` binds to them. The predicate contract's docblock states the fault semantics the consumers are held to: a faulting field-rule predicate refuses the SUBMIT naming the field and the rule; visibility stays fail-open at RENDER; a blank or faulting gate predicate is diagnosed, never a silent true. The evaluation helper's fallback stays freely specifiable. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
The ADR-0087 D3 semantic entry for the D1 narrowing, the pin test for all three field-rule slots, and the two ADR anchors. ADR-0089 gains a pointer: it unified the `*When` family's NAME, this record decides what the family does when it cannot run. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
…ed predicate `gen:docs` was the one artifact `check:generated` proved stale; the published reference now states `source` as required on `Predicate` / `PredicateInput`. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
`expression()` and `cel()` (hence `F` and `P`) always set a non-blank `source`, but were declared as returning `Expression`, whose `source` is optional. Harmless while no slot required `source`; once the field-rule triad did, a `P` template written straight into `visibleWhen` became a TS2322 — the recommended authoring form for a predicate stopped type-checking in the one place predicates are written (caught by `check:skill-examples` on the docs and skills corpus, and by the examples typecheck on app-showcase). Fixed at the producer, not the call sites: the return type now states what the helper emits. Narrowing a return type removes nothing from a caller — `EvaluatedExpression` is assignable to `Expression`. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
…eld-rule-predicate-fault-semantics
|
…/main The declaration-text pins main brought in #18971 move with this branch's two facts: `source` becomes required wherever the predicate contract composes, and `cel` / `expression` now declare the `EvaluatedExpression` they always emitted. Exactly 8 distinct changed lines across all six files; the line count is those two facts repeated at every composing site. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
… schema ADR-0136 D1 binds the `FieldSchema` field-rule triad to `PredicateInputSchema` from another file, and alias resolution in the D7 discovery scan is FILE-LOCAL — so all three surfaces dropped out of discovery and their covers went STALE, 34 positions found against a `head` floor of 37. The ledger header had written this trap down as limitation 2, naming this exact alias as latent; this change makes it live, so the roster gains it and the limitation is corrected to say so. ⛔ No floor was lowered and no ledger row was deleted: the three surfaces still exist and are still exactly what the scan looks for. The roster docblock already prescribed this repair for a new alias. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
|
PM note — D4's carrier is filed, two numbers in the record are off, and the PR body is stale. None of this is a verdict; the at-tier clause-② review is still running. 1. The follow-up ADR-0136 promises now exists: #19000. The Scope boundary section says of the gate-slot conversion: "That conversion is filed as a follow-up with the slot inventory, and it is the one place where D4 is currently declared ahead of its authoring-side enforcement." #19000 is that follow-up and carries the inventory. It is The sentence in the ADR does not cite a number, so the reference is still dangling in the text. Adding 2. Two numbers in the Scope boundary are off, both in the direction that strengthens its own argument. Instrument:
Both readings went to the reviewer as evidence to check, explicitly not as a verdict and explicitly not as grounds to move its PASS or FAIL. 3. The PR body was written once and now predates three developments. It has not been rewritten since it was opened, and three things landed on the branch after it: the DEBT producer fix, the D7 roster entry in 4. Landing posture, restated so it is on the record and not only in a seat post. This PR touches Generated by Claude Code |
Clause-② review returned — PASS with findings, and this PR is HELD. Carriers stay on.Read this as the seat's disposition, not as the record of record. No Review admissibility. Isolated at-tier review, dispatched because this seat serves below Verdict: CLAUSE-② REVIEW: PASS. Accept-set narrowing on the Why it is held anyway — one blocking finding, and it is not a clause-② matter. The reviewer found, and I have independently confirmed against primary sources, that this record is silent about a standing ruling that covers part of its own subject:
So the record's Scope-boundary rationale — that converting the gate slots "would bake a direction the ruling did not give" — is true of batch #119 and silent about batch #122, which gave exactly that direction for those slots. A governed record must not reach its approver with an incomplete account of the standing rulings on its own subject. That is the hold. Also established, separately from the review. This record's ADR number collides: Why no record and no carrier strip on this head. The gate reads the PR's live head ( Not mine to decide, and going to the maintainer. Which binding the record should describe, whether this PR's ADR-0087 entry folds into or cross-references Disposition of the follow-up card I filed earlier today. #19000 is closed as a duplicate of #15811. It re-opened a question batch #122 had already settled. My error, recorded on both cards. Non-blocking, carried for whoever edits the record: the migration entry's Seat: Generated by Claude Code |
Contract reviewServed-tier: 209/209 ① Derived judgmentsIsolated at-tier re-review. The earlier PASS on this PR judged Ruling A's REMOVE list is measured removed, byte-exactly: 16 non-diff paths of the former 22-file set carry blob SHAs identical to the pinned parent The renumber holds on an independently widened instrument: ② Semver level
③ Boundary flagsGate-weakening: none. Implemented-by: VERDICT: PASS Landing is additionally gated twice over: Non-blocking, carried: ADR-0137's Generated by Claude Code |
…eld-rule-predicate-fault-semantics # Conflicts: # packages/spec/src/shared/expression.zod.ts
…eld-rule-predicate-fault-semantics
…/main Discharges the os-regen deferral from the prior merge commit. root.txt and shared.txt now reflect main's #18638 evaluated-expression-slot narrowing (source becomes required across the composing slots) plus the drift picked up while catching this branch up to main's current tip. No hand edits.
Main shipped the identical /`expression` return-type narrowing to EvaluatedExpression first, under #18638 (card #15811) -- confirmed by the merge: both sides made the same change independently, and ADR-0137's own status line says its PR carries no schema change. Re-announcing that narrowing here would duplicate #18638's own changeset entry in the same release. Drop the redundant paragraphs and keep only what #17778 alone ships: the ADR-0137 predicate fault-semantics contract and its ADR-0089 addendum.
…ion's two halves one call (objectstack-ai#19005) Part of objectstack-ai#18670 — item 2, the **third** of the ruling's four named arms. objectstack-ai#18670 remains open: banned keys is still untaken, and this body deliberately carries no closing keyword for that number. Clause-②: yes (narrowing) Director ruling batch objectstack-ai#154 item 3, letter **C** (comment 5725370614, maintainer 「同意」): 「the projection emits a refinement only where the rule is a complete, mechanically derivable JSON Schema pattern — banned keys, required-one-of, non-blank — one ledger row at a time; everything else stays annotated as `x-dropped-refinements`」. Continues PR objectstack-ai#18952 (squash `5e5ec9fa42194723cc523a274e7221c8447c4487`), which landed `required-one-of` and `non-blank-string`. ## 1. The arm: `dependentRequired` `data/SSLConfig`'s refinement is `hasCert === hasKey` — precisely `dependentRequired { cert: ['key'], key: ['cert'] }`. It is emitted through the same closed-vocabulary mechanism the previous arm built: `src/shared/refinement-projection.ts` declares, `scripts/lib/refinement-projection.ts` emits. No second mechanism was introduced. **Exact, not approximate.** A key absent from a JSON object is the only way for its value to read `undefined`, and `dependentRequired` triggers on PRESENCE — so a key present with any JSON value, `null` included, arms its dependency exactly as the predicate's `!== undefined` does. The dependency map is read once into the declaration and the predicate reads it from there, so the published keyword and the enforced rule cannot name different keys. ### Ledger: the rows retired, by name `packages/spec/dropped-refinements.baseline.json`, **201 entries / 553 sites → 200 / 551**: | row | before | after | |:---|:---|:---| | `data/SSLConfig` | `sites: [""]` | **deleted** — drops nothing now | | `data/SQLDriverConfig` | `sites: ["", "sslConfig"]` | `sites: [""]` — the `sslConfig` site closed | 1 row deleted, 1 row shrunk, **2 sites closed, 0 sites added anywhere**; the ledger diff is deletions only. Generator census after: 551 dropped across 200 published schemas, **199 projected** — 137 `required-one-of`, 60 `non-blank-string`, **2 `dependent-required`** — 3 undecidable. `data/SQLDriverConfig`'s remaining `""` site is its **own** separate rule, "`sslConfig` is required when `ssl` is **true**". That judges a VALUE, is `if`/`then` rather than this arm, and correctly stays dropped and annotated. ### Banned keys (`propertyNames` / `not`) — NOT taken, and not forced Confirmed against the tree, not assumed: the nearest sites judge a banned VALUE on a string (`FILTER_ARRAY_LOGIC_KEYWORDS`) or an allowed key set that is data-dependent (`ai.paramHints` against the action's own params). Neither is mechanically derivable, so **no candidate was constructed**. This is why the body says `Part of` and carries no closing keyword. ## 2. Mechanism fix A — the verdict is per NODE, the rules are per CHECK `verdictFor` compared a node with ALL custom checks against the node with NONE, so any one declared arm marked the whole node `projected`. Reproduced on the landed code before changing it: ``` mixed(declared+undeclared) dropped: [] projected: [{count: 2, declaredPatterns: ['non-blank-string']}] undeclared-alone (lit) dropped: [{count: 1, declaredPatterns: []}] projected: [] declared-alone (lit) dropped: [] projected: [{count: 1, declaredPatterns: ['non-blank-string']}] ``` A second refinement on a declared node was therefore neither ledgered nor annotated, and the generator's UNDECLARED line could not see it — silently violating the ruling's own 「A refinement that is not one of these named patterns stays dropped and annotated」. **Fix:** `projected` now requires `customs.length === declaredPatterns.length`; anything else is `dropped` conservatively. The RAW differential is kept as a new `projectionMoved` field so the detector still MEASURES rather than asserts — collapsing it would have made the instrument blind to the zod upgrade it exists to notice — and the generator prints partially-stated sites on their own line. **Ablation, both directions** (anchor-verified on disk, `scripts/ablation-replace.mjs`): | leg | blob | result | |:---|:---|:---| | mutated — drop the `total === stated` guard | `54ed82dbe4c2` to `2c1bff777363` | **1 test red**, 42 green: "a DECLARED arm beside an UNDECLARED rule stays `dropped`" | | restored | back to `54ed82dbe4c2`, `git diff HEAD` empty | **43 / 43 green**; mutant text on disk 0, guard text 1 | ## 3. Mechanism fix B — generator/detector coupling, by construction `build-schemas.ts` (three `toJSONSchema` calls) and `projectOrNull` each passed the `override` independently. **Measured on the pristine base** with only the generator's import stubbed out: | leg | gate exit | `shared/Expression.json` `allOf` | `x-dropped-refinements` | files carrying the non-blank pattern | |:---|:---|:---|:---|:---| | base, untouched (dark control) | 0 | present | absent | **35** | | base, generator-side override dropped | **0 — GREEN** | **absent (wide)** | **absent (SILENT)** | **0** | Census identical to an untouched run (553 / 201 / 197). That is the item-1 silence restored, standing behind a green ratchet — worse than the state the card was filed about, because the ledger now certifies it. A merge-conflict resolution was enough to cause it. **Chosen fix: one shared projection helper** — `projectPublishedJsonSchema` in `scripts/lib/refinement-projection.ts`. All three generator calls, the union-branch projector behind the third, and the detector's differential now reach `z.toJSONSchema` through it, and `projectByPruningUnionBranches` no longer takes an `override` option at all. There is no argument left for a caller to forget. **Why the sandbox-builder pin was rejected**, not overlooked: a pin *detects* after the fact and can be skipped, deleted or made vacuous, and it leaves the two-argument shape in place so the next merge conflict can still separate them. The choke point makes the one-sided failure **unrepresentable** rather than caught. Both halves now lose the override together or not at all — which is what turns the ablation from silent into loud. The test file's own `publish()` helper was rewired through the same call for the same reason, so the unit pins measure the real seam rather than a re-spelling of it. **Ablation, both directions:** | leg | gate exit | `Expression.json` `allOf` | `x-dropped-refinements` | |:---|:---|:---|:---| | mutated — override removed from the ONE helper | **1 — RED**: 46 undeclared schemas + 76 miscounted ledger entries | absent (wide) | **present (annotated)** | | restored | 0 | present | absent | The contrast is the whole point: before, one-sided removal was green and silent; now it is red **and** the file confesses. ## 4. Contract: the published file narrows toward what the runtime already refuses **Whole published tree, base vs head:** 1530 of 1532 files byte-identical. The two that move are `data/SSLConfig.json` and `data/SQLDriverConfig.json`, each gaining `dependentRequired` and losing the matching `x-dropped-refinements` row. Nothing else in `packages/spec/json-schema/**` changed. **Parse-equivalence probe — 10,368 documents** (2,592 SSLConfig-shaped over the full presence lattice of 4 keys times 6 value shapes including `null`, a wrong type and an unrecognised extra key; 7,776 SQLDriverConfig documents embedding each of those under three `ssl` states). Published-side verdicts computed with ajv 8.20.0 (draft 2020-12) against the two real snapshots. | reading | SSLConfig | SQLDriverConfig | |:---|--:|--:| | documents | 2,592 | 7,776 | | runtime accepts | 60 | 180 | | published accepts, base | 27 | 54 | | published accepts, head | 15 | 30 | | **narrowed by this arm** | **12** | **24** | | widened | 0 | 0 | | **documents the runtime ACCEPTS that the published file now refuses** | **0** | **0** | **Runtime behaviour did not move.** The runtime verdict vector is byte-identical at merge base and head over all 10,368 documents — sha `9e7c848f04e0c687` (SSL) and `4f18f835d4d1a62e` (SQL) on both sides. The base leg was run against the real base blobs (`git checkout` of the two source files at `d8b12fca9`, blob hashes asserted both ways, restore proven by an empty `git diff HEAD`), not against a retyped predicate. **LIT CONTROL for that zero** — weakening the dependency map to one direction (`{ cert: ['key'] }`) moves **96 documents** (24 SSL + 72 SQL) and lifts runtime accepts from 60 to 84 and 180 to 252. The zero is a reading, not a silence. Note the published-accepts figures sit below runtime-accepts on both sides: `SSLConfig.json` is the OUTPUT shape and lists `rejectUnauthorized` as required because the runtime applies its `.default(true)`. That asymmetry is pre-existing, is the `x-io` convention, and is unchanged by this PR — it is reported rather than netted out. ## 5. Verification - **Gates:** derived from the merge base with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, re-derived after the `origin/main` merge (identical, **84 commands**). Every exit code captured by redirecting to a file first, never through a pipe. **80 exit 0, 0 findings.** The remaining 4 — `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt` — exit **3**, which those gates define as `PREREQUISITE NOT MET` ("Nothing was measured ... It is NOT a finding"): each reads BUILT output of packages outside this diff. They are **NOT MEASURED**, not red; the re-run against a full build is reported on the card. - The derivation's own caveats are carried, not netted out: 50 artifact-roster families score `silent` for every card in the tree, 11 declare a population too wide to place, 5 take a value from the workflow, and 5 path-scheduled CI jobs run 30 steps with no local invocation. None of those is a clearance, and CI owns them. - **`pnpm --filter @objectstack/spec check:generated`:** all 16 generated artifacts up to date. **`content/docs/references/**` does not move** — see acceptance notes. - **Targeted tests:** `scripts/refinement-projection.test.ts`, `scripts/dropped-refinements.test.ts`, `scripts/union-branch-projection.test.ts` — **91 / 91**. `packages/spec` typechecks clean (`tsc --noEmit` over both the package and `tsconfig.scripts.json`). The full `@objectstack/spec` suite reading is on the card. - **Lint, declared narrowing:** eslint run over the 9 changed lintable files, 0 errors / 0 warnings, file count read from `--format json`. The population is `eslint.config.mjs`'s own `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`; the config states in its own words that this repo "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file", so this diff cannot move the verdict on a file it does not touch. The repo-wide sweep is CI's run. - `origin/main` merged through `bash scripts/pm/os-regen-merge.sh` (no rebase, no force-push). It brought one docs-only commit, objectstack-ai#18979, overlapping none of this branch's paths and no `merge=os-regen` path. The previous arm's implementation body was asserted still present by quoted-exact-name `git grep` against `origin/main`, with a dark control at 0. ## Acceptance notes Noted, not filed — out of scope for this card and not one of the three filable classes: - `packages/spec/scripts/build-schemas.ts` (the authorable-surface docblock, near line 846) still names the retired `api-surface-signatures.json`. The previous seat handed this to "the next editor of `build-schemas.ts`", which is this PR. It is left untouched deliberately: it is a stale code comment, not a defect, a contract violation or an authoring trap, and the bounded in-place exemption requires the finding to be **the same defect class as this card**, which it is not. Carrier: the next PR that edits that docblock for its own reasons. - The dispatch's overlap warning — that a new keyword might move `content/docs/references/**`, four pages of which open PR objectstack-ai#18985 edits — **measured FALSE**. `dependentRequired` is a sibling keyword the reference renderer does not read, `check:docs` is green and `check:generated` reports all 16 artifacts current. No reference page moves, so there is no collision with objectstack-ai#18985 on that directory. Reported for the seat to file (a candidate class-(b) finding, deliberately NOT fixed here): - `packages/spec` ships `src/**/*.zod.ts` in `files[]`, and `scripts/check-published-files.mjs` allows it with the reason "The Zod schemas are themselves the contract (Prime Directive objectstack-ai#1); **downstream code imports them directly**, so these sources are product rather than build input." Two measurements contradict that reason: (1) the package's `exports` map exposes no `./src/*` subpath and no wildcard, so no consumer can import those files at all; (2) 188 of the 202 shipped `*.zod.ts` files carry a relative import resolving to one of 35 modules under `src/` that the glob does NOT ship (`src/shared/lazy-schema.ts` alone is imported by 181 of them), so they would not resolve even if reachable. Overwhelmingly pre-existing and far outside this card; this PR adds the third importer of one of those 35. Not verified by `npm pack` and not by a real consumer import — that is the next step for whoever takes it. --- _Generated by [Claude Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…site trace-sampling condition (objectstack-ai#19084) Fixes objectstack-ai#18118 Clause-②: yes — retiring a published authorable surface. Carrier: the changeset `.changeset/18118-retire-observability-cel-arms.md`, which declares the same line and the ADR-0087 disposition. ⛔ The `needs:contract-review` label is the seat's act; this PR neither hangs nor clears it. Ruling: batch objectstack-ai#160 item 3, **letter A**, maintainer 「同意」 2026-09-18T11:59Z — retire the two CEL expression arms under ADR-0049 enforce-or-remove, by the `spec-property-retirement` playbook. ## What was removed Two union arms, not two keys: | slot | was | is | | --- | --- | --- | | `ServiceLevelIndicatorSchema.successCriteria` | `z.union([{ threshold, operator, percentile? }, EvaluatedExpressionInputSchema])` | the structured object alone | | `TraceSamplingConfigSchema.composite[].condition` | `z.union([ StructuredFilterRecord, EvaluatedExpressionInputSchema ])` | the structured filter record alone | `EvaluatedExpressionInputSchema` itself is untouched — `packages/spec/src/shared/expression.zod.ts` is not in this diff at all (it is held by PR objectstack-ai#18985, which is not addressed here). What left is two references to it. ## The card's zero, re-derived — and the instrument's radius Re-derived on this branch's base `176b03582e600ee5628d21bff9422073c5a5530c`, not inherited. `git grep -n` over the whole tracked tree for `successCriteria`, `ServiceLevelIndicator` and `TraceSamplingConfig`. Every hit outside `packages/spec/src` is a generated artefact (`api-surface*`, `authorable-surface*`, `authorable-defaults`, `declaration-map`, `export-origins`, `json-schema.manifest`, `dropped-refinements.baseline.json`), a reference page, a changelog or changeset, or the shipped skill's prose row. Inside `packages/spec/src` the readers are: the two schemas' own unit tests, `shared/evaluated-slot-population.test.ts` (a census), the migration registry's prose, and one docblock in `shared/evaluated-slot-union.ts`. Outside the spec package the only reader is `packages/qa/dogfood/test/expression-conformance.ledger.ts` — a classification ledger, not an evaluator. **No service, plugin, runtime or CLI path reads either key.** **Reachable radius of the instrument:** tracked files in THIS checkout at THIS commit. It does not reach untracked or ignored build output, another repository, or a published npm tarball. **One known target outside it:** the sibling repository `objectstack-ai/objectui`, which is on this box but is a different git repository, so no `git grep` here can see it. It is named rather than waved at, because the `template-title-format` row of the same ledger records exactly this limit for a different key: an interpolation site that lives there and cannot be measured from here. ⛔ **The argument that used to stand here was REJECTED by at-tier contract review and is withdrawn.** It claimed the radius was closed by a positive fact — that nothing in a sibling could evaluate these slots without importing the symbols naming them, whose consumers `export-origins/` and the `Console Pin Gate` enumerate. That is wrong on three counts the review measured: `export-origins/` records by its own description which SOURCE DECLARATION each exported name resolves to — origins, NOT consumers; the `Console Pin Gate` is path-filtered and was SKIPPED on this very PR; and an evaluator need not import either symbol, since a REST-served metrics config can be read by key. **What closes the radius instead is direct measurement outside it, with a lit control in each repo.** objectui @ `3e4f6324f7`: `successCriteria` 0 files, `ServiceLevelIndicator` 0, `TraceSamplingConfig` 0; controls in the same run — `visibleWhen` 340 files, `ObjectSchema` 652. hotcrm @ `087b7c5dc4` (887 tracked files; public, served by this session's git proxy — an earlier dispatch's 「unreachable」 was the seat's error): the same three at 0 plus `slis` 0; controls — `visibleWhen` 15, `defineStack` 47, `@objectstack/spec` 269. `sampling` shows 4 files there and **every one was read** (an MCP capability table row, two prose sentences, a CHANGELOG line — no trace-sampling config), so that zero stands on inspection and not on the count. **Known targets still OUTSIDE the radius, named rather than waved at:** `objectstack-ai/cloud` (access denied to this session) and any third-party npm consumer of `@objectstack/spec`. Neither was measured, by anyone. What BOUNDS the cost of a wrong zero there is the retirement's **audibility** (a bound, ⛔ not a closure — at-tier review's wording): a surviving predicate is a `tsc` error or a parse refusal carrying the prescription, never a silent change. **Every symbol was located by its declaration site**, and a literal inside a `//` or `/** */` comment was counted as prose, not as a reader — that is why `shared/evaluated-slot-union.ts` is listed as a docblock and `migrations/registry.ts` as prose. Exit codes were captured before any pipe. ## The retirement kit - **The prescription hangs on the surviving schema's own `error` map**, dispatched on `issue.input` (the `HookBodyCapability` / `object.managedBy: 'system'` pattern). `retiredKey()` and an ADR-0087 D2 strip both retire a KEY; neither retires an ARM, and the keys survive here. - **Where the prescription reaches, measured on zod 4.4 and pinned both ways.** A schema's `error` map is consulted for the top-level `invalid_type` a NON-OBJECT raises and not for the child issues a wrong-shaped OBJECT raises. So on `successCriteria` the bare-string spelling carries the prescription and the `{ dialect, source }` envelope is refused by the structured arm's own missing-key issues; on `condition` both spellings carry it, because the record arm's aborting `dialect` refine sees the object itself. The negative is pinned too: a value refused for a reason that is NOT the retirement must not borrow its sentence. - **ADR-0087 disposition: a D3 SEMANTIC entry**, `observability-cel-predicates-retired` (`packages/spec/src/migrations/entries/semantic/18.observability-cel-predicates-retired.ts`, with `registry.ts` **regenerated**, never hand-edited between the markers). A predicate is an intent no threshold/operator pair or attribute filter records. Both prescriptions therefore carry **no** `os migrate meta` sentence — owed only where a conversion covers the surface. - **Changeset** with the FROM → TO table and the one-line fix, `@objectstack/spec` minor with the BREAKING banner, per the ruling's Execution section. - **Baselines and reference pages regenerated**, never hand-edited: `api-surface-declarations/`, `authorable-defaults/`, the two `content/docs/references/system/*.mdx` pages. ## Acceptance notes - **FOUR published JSON Schemas change projection direction, mechanically** (corrected from 「Two」 by at-tier contract review — see below). The retired arm held the last `.transform()` in the `system/MetricsConfig` and `system/TracingConfig` subtrees, so both defs now project in output mode instead of falling back to the input shape. Consequences, all declared in-diff: `system/MetricsConfig:slis` and `system/TracingConfig:sampling` publish the `default` the parser has always applied (declared in `DEFAULT_CHANGES_BY_MAJOR` with the `ai/KnowledgeSource:refresh` row as the precedent, that mechanism run backwards), and the nested type cells of both reference pages lose the `?` from their default-bearing keys — the output-mode signature, and the same convention every transform-free def in the repo already publishes under. **No runtime default moves**: measured by byte-identity of the untouched `.default(…)` and by parsing a minimal config on the built package. - **Consumers swept beyond the named file surface, because they break otherwise.** The `evaluated-slot-population.test.ts` census drops 36 positions over 34 declaring lines to 34 over 32, naming both departures rather than subtracting them; the `evaluated-slot-union.ts` docblock drops five of 36 to three of 34; the ADR-0058 D7 ledger row `cel-declared-unwired-observability` closes with the removal, and its companion test's `inline` scan floor drops 3 to 1 with both positions named, exactly as that floor's own instruction requires. - **The ruling's Execution section says `Clause-②: no`; the dispatch claim comment says `Clause-②: yes`.** This PR carries the claim's line, because the claim comment is the carrier the clause-② check reads and the two must agree. Flagged rather than silently chosen. The `yes` reading also has independent support in this diff: two published JSON Schemas change projection direction. - **Noted, not filed:** the reference pages' inline nested type cells render post-parse optionality (a defaulted key reads as required) while the expanded `Nested Shape:` sections below them read the zod node and say `optional (default: …)`. The two disagree for every output-mode def in the repo, not only these; it predates this card and this diff does not widen it. Carrier for anyone who picks it up: `packages/spec/scripts/build-docs.ts`. - **Not in this diff, by the ruling:** the structured arms (their own card); `skills/objectstack-formula/SKILL.md`, whose `structured | cel` row for `metrics` / `tracing` is the skills lane's at tier; and `packages/spec/src/shared/expression.zod.ts`. ## Verification Run under the shared verify lock; the judged line of each is quoted in the report on the card. --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ - **Same-major absorption (added after at-tier contract review found it missing — the round's one BLOCKING finding).** The same unpublished step 18 carried `entries/semantic/18.evaluated-expression-slots-source-required.ts`, which still enumerated these two slots among 「the 36 declaring positions」 and still told an upgrader to give a sampling `condition` a dialect and a non-blank `source` — **the exact envelope this head refuses**. The playbook's same-major rule applies to the published D3 record exactly as it applied to the census test and the helper docblock. That entry now reads 34 positions, drops the two slots and the `condition`-specific sweep clause, and routes a hit at either slot to `observability-cel-predicates-retired`. Verified in the GENERATED output, not only the input: `registry.ts` carries `34 declaring positions` once and `36 declaring positions` **zero** times. - **Why D3 is right rather than merely available.** Both error-map precedents this retirement copies its MECHANISM from also registered a D2 conversion, because for them a mechanical rewrite existed. Here none does: a strip leaves a REQUIRED `successCriteria` missing (the SLI stops parsing) and a composite branch with no condition at all. - **The four defs, named** (the two nested ones were disclosed nowhere before): `system/MetricsConfig` (`default` on `slis`, plus 8 `required` members) · `system/TracingConfig` (`default` on `sampling`, plus 4) · `system/ServiceLevelIndicator` (one `required` member, `enabled`) · `system/TraceSamplingConfig` (one `required` member, `rules`). ⭐ The last two are invisible to the `default-changes.ts` table **by the ratchet's construction**, not for lack of a default: that table records default VALUES per key, and `enabled` / `rules` already carried theirs (`true`, `[]`) published at the base and unmoved here, so no row of it can express a `required` growth. ⛔ Corrected from an earlier wording of mine that said they had 「no default to declare」 — at-tier contract review measured that as loose; the exact form is the changeset's own: only the first two carry a `default` MOVE. --- _Body edits above made by the `domain:spec#4` seat after at-tier contract review; the dev writes the body once, at creation._ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… is a credential (objectstack-ai#18335) (objectstack-ai#19322) Fixes objectstack-ai#18335 Clause-②: no ⛔ **Governed surface — this PR parks as a draft by design.** `docs/adr/**` is Tier H (Prime Directive objectstack-ai#14). No ready-flip, no enqueue, no auto-merge, and no approval by any agent seat. An authorized human approval is owed before this lands; a draft with the work done is the complete deliverable. ## What this lands ADR-0090 D10 rule 4 read 「every write records `performed_by` (agent) + `on_behalf_of` (user) + run id」. No door has ever read it that widely, and the gap had never been written down. Per the ruling on the card (comment 5690859150, batch objectstack-ai#139 item 1, letter **A**, maintainer 「同意」 2026-09-16, reaffirmed at 5731818788), this PR closes it documentarily: 1. **Rule 4 is narrowed** to *every write by an agent principal*, and it now says what an agent principal IS — a caller a door resolves to `principalKind: 'agent'`, today the OAuth / MCP client door alone — so a later reader can classify a new caller type without re-litigating. It also states the positive reading of the absence: a missing `performed_by` is the record that the principal acted for itself. 2. **A dated note** at the end of D10 (`Note (2026-09-16, objectstack-ai#18335)`) records that an API key is its owner's **credential**, not an agent principal, and *why* — a credential is how a principal acted, never a second who — plus what was refused (API keys as a principal category of their own) and on what basis. Documentary only. **No code**: one file, +47 / -2. ## The ruling's premise, measured rather than inherited The ruling asserts that API-key writes audit as the owner today. A note that misdescribed the enforcement would recreate the very defect this card closes, so the assertion was measured first. All readings against `origin/main` at base `e3b3cdd`, taken 2026-09-20T11:05–11:15Z. | reading | result | |---|---| | the one seam that produces an agent principal | `packages/core/src/security/assemble-execution-context.ts#entryFields` — `const agent = !anonymous && oauth?.clientId ? oauth : undefined`, consumed by `principalKind`, `onBehalfOf` and `performedBy` | | `principalKind` / `onBehalfOf` / `performedBy` in `packages/core/src/security/api-key.ts` | **0 / 0 / 0** — lit control: `userId` reads **6** in the same file | | the same three in `packages/core/src/security/resolve-authz-context.ts` | **0 / 0 / 0** — lit control: `userId` reads **48** in the same file | | the same three in `packages/runtime/src/security/api-key.ts` | **0 / 0 / 0** (a 25-line re-export module) | | which doors honour an API key, and what each hands the assembler | **all three** that run `resolve-authz-context.ts#resolveAuthzContext`. REST and MCP **stdio** pass `oauth: undefined` **by construction** (`rest-server.ts`, `mcp/src/plugin.ts#resolveStdioExecutionContext`); the runtime / MCP **HTTP** dispatcher excludes keys with a **guard**, `resolve-execution-context.ts#extractJwtBearer`, refusing an `osk_`-prefixed or non-JWT bearer | | repo-wide non-test writers of `performedBy` | the MCP/OAuth seam, the spec + hook declarations of the field, and the audit writer that reads it. No API-key path | ⇒ an API-key caller falls through `agent ? 'agent' : anonymous ? 'guest' : 'human'` to `human`, carries no `onBehalfOf` and no `performedBy`, and its `sys_audit_log` row is the owner's own. **The measurement agrees with the ruling's premise**, so the narrowing describes the enforcement rather than changing it. ## Was there a dangling D6 sentence? No. The clause 「explain (D6) reports both sides of the intersection」 lives *inside* rule 4, so the narrowing carries it. Measured on the ADR: `attribution` occurs once in the whole file (rule 4) and `both sides` once (its second line). The companion `docs/design/permission-model.md` does not restate the rule at all — `performed_by` / `performedBy` / `attribution` / `every write` read **0** there, against a lit control of **14** for `agent`. The generated `content/docs/references/**` tables carry the field's own `.describe()` text, which already says 「Set only at the /mcp OAuth door … absent everywhere else」 — already narrow, nothing to correct. ## Gates Derived in this worktree from the real change set and reconciled with the run log: ``` node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack # 18 commands node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RAN_FILE Run reconciliation — 18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN. EXIT CODES — all 18 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them. ``` All 18 exit 0, each captured to disk **before** any pipe. `check-adr-links`, `check-adr-symbol-anchors` (+ both self-tests), `check:adr-anchors`, `check:doc-authoring`, `check:nul-bytes`, `check:pm-governed-merges`, `check:pm-prior-rulings`, `check:comment-mask-corpus`, `check:cross-package-test-inputs`, `check:driver-memory-census`, `check:refd-timer-probe`, `check:watch-hint-literal`, `check:ci-filter-parity`, `check:closing-keyword-parity` (+ self-test) are among them. `check:doc-formula-expressions` first exited **3** — `PREREQUISITE NOT MET`, the gate's own "nothing was measured" code — and was re-run to exit 0 after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint`, taken under the shared verify lock. The three new symbol anchors in the note (`#entryFields`, `#resolveApiKeyAdmission`, `#resolveAuthzContext`) resolve as `declaration` class; no line-number anchor was introduced. ## 维护者速读(草稿) **改了什么。** 只动一份决策记录(ADR-0090),一个文件,47 行增、2 行删。一句原本写着「**每一次**写入都要记下『谁代谁干的』」的规则,被收窄成「**代理主体**的每一次写入」;同一节末尾新增一段带日期的说明,写明 **API key 不在这条规则里,以及为什么**。⛔ 没有改任何代码,平台行为一行都没变。 **为什么改。** 平台里只有一个地方会把调用方判成「AI 代理」:MCP 的 OAuth 门。**三个门都认 API key**,但没有一个会把它变成代理:REST 与 MCP stdio 结构上就不传 OAuth 凭据,MCP HTTP 门则靠一道两行的**守卫**挡住 `osk_` 开头的 bearer。三条路都只认出**钥匙的主人**,所以审计日志记的就是主人本人——这一点本轮在源码上实测过,与裁定的前提一致。于是规则写的是「每一次」,实现做的是「只有代理那一次」,这就是**声明面与执行面对不上**。维护者已裁 A(API key 是主人的**凭证**,不是第二个「谁」),裁定里同时明写:只维持现状而不改那句话,等于新留一条「说的和做的不一致」。这份 PR 就是把那句话改对,并把理由记在案。 **风险与代价(含回滚)。** 风险低:纯文档,无运行时、无 API、无数据结构变化,不发布任何 npm 包。真正的代价是**语义上的**:这条规则从此明确**不**覆盖 API key。将来若出现合规要求「要分得清是人按的还是钥匙跑的」,那是一张新卡、新决策,而不是重读这一条——这一点也写进了 Note 里。回滚是一次 `git revert`,零迁移、零数据影响。 **席位意见。** **你要做的(一个动作)。** 读一遍 D10 rule 4 那六行和它后面那段带日期的 Note,回「同意」或指出要改的措辞。这是受管面(`docs/adr/**`,Tier H),在你点头之前它会一直停在 draft。 ## Acceptance notes - **`skip-changeset` is owed and was deliberately NOT applied.** This diff publishes nothing (`docs/adr/**`), so `Check Changeset` needs that label; the dispatch forbids this executor from touching any label on a governed-surface PR, so the label is left to the seat. Until it is applied, `Check Changeset` is expected red and that red is not a finding about this diff. - **Noted, not filed — D10's 2026-07 Status blockquote still lists 「the agent audit-provenance gap」 as an open follow-up.** That gap is the one objectstack-ai#17022 covered, and that card is `closed`/`completed` (2026-09-16T06:41Z). This is a stale pointer in prose, not a reproducible defect, not a contract violation and not a metadata-authoring trap, so it is not one of the three filing classes. Carrier: **objectstack-ai#18374**, which already owns the residual close-out in the same D10 area. - **Seat assumption 2 re-measured wider than dispatched.** The dispatch named four PRs; all **36** open PRs in the repo were read at 2026-09-20T11:10Z (`GET /pulls/{n}/files`). Exactly one touches `docs/adr/**` — objectstack-ai#18985, on ADR-0089 and ADR-0137, disjoint files. Zero open PRs hold ADR-0090. One caveat recorded rather than hidden: PR objectstack-ai#17076 (`chore: version packages`) has more than 200 files; pages 1 and 2 were read (200 files, zero `docs/adr/` hits) and the tail was not enumerated. - **A small line drift in the dispatch's measurement table, reported as instructed.** The card body cites the agent channel at `:293` consumed at `:316`/`:317`; on `e3b3cdd` those are `:294`, `:317`, `:318` — the file gained the `performedBy` line from objectstack-ai#18371. The rule 4 sentence itself was anchored on its text and matched verbatim, at line 391 as the seat read it. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17778
Clause-②: no
The
domain:spechalf of the maintainer ruling on objectui#8069 (decision batch #119 item 3, 2026-09-12: 「同意」 to A, with Q2 yes and Q3 yes), reworked under decision batch #160 item 1 on #19003 (letter A, maintainer 「同意」 2026-09-18T11:58Z). The renderer half is objectui#8069 and is not in this PR.What this PR is now — a record, one producer fix, and nothing else
Ruling A removed the schema change from this PR. Decision batch #122 item 2 (card #15811, comment
5644350409, 2026-09-12) had already ruled the evaluated-slot narrowing across all 36 declaring positions — its own census names 「field / option / grid-columnvisibleWhen/readonlyWhen/requiredWhen」 — and PR #18638 owns it under one ADR-0087 id and lands first. Card #17778 ruled fault semantics, not the carrier symbol, so nothing ruled is lost.Removed here (commit
9f30a18a9): the threeFieldSchematriad-slot edits; thePredicateSchema/PredicateInputSchemarebinding, which go back to composing the persistence schemas, wide; thefield-rule-predicate-evaluated-slot-source-requiredADR-0087 entry (the entry file and, throughgen:migration-registry, its registry rows) and the changeset marker; the triad pin test; the two ADR anchors; and every api-surface / reference-page row that existed only because of those.Kept, per the same ruling: ADR-0137 (the fault-semantics record, renumbered — see below), the ADR-0089 pointer addendum, and the producer fix.
Re-derived and removed (see the measurement below): the ADR-0058 D7 roster entry.
The revert is byte-exact, not "close enough"
The three sources and the
field.zodanchor were restored withgit checkout d8b12fca97 --(the merged-main parent of this branch's merge commit, a pinned sha, not a moving ref);git diff d8b12fca97is empty for each.registry.tswas not hand-edited — the entry FILE was deleted andgen:migration-registryre-emitted the generated regions; the result is byte-identical to merged main andcheck:migration-registryis green.After regenerating, the whole diff against merged main is 6 files, and the generated half of it is 4 lines:
content/docs/references/**packages/spec/api-surface-declarations/root.txtandshared.txt, all of them the producer fixpackages/spec/api-surface/,export-origins/,declaration-map/check:api-surfacegreenThe producer fix, on its own terms
cel()andexpression()(henceFandP) always write a non-blanksource, but were declared as returningExpression, whosesourceis optional — a declaration of a shape neither function can produce. The fix is at the PRODUCER (Prime Directive #12): the return type now states what the helper emits. The docblock was rewritten so it no longer rests on the triad requirement this rework removes; what it now says is the general fact plus the live consumer,FlowEdgeSchema.condition.Narrowing a return type removes nothing from a caller —
EvaluatedExpressionis assignable toExpression— so no call site changes.Clause-②re-judged, and the changeset level follows itThe previous body declared
Clause-②: yes (narrowing). With the accept-set narrowing gone that declared something this diff no longer does, andCheck Changesetread it and reddened. Re-judged against the six files that remain:Clause-②: no. The judgement is measured, not asserted:packages/spec/srcchanges are two return types and two docblocksgit diff d8b12fca97 -- packages/spec/src9f30a18a9check:api-surfacegreen, with no removed-or-narrowed report⇒ the changeset is
patch, the BREAKING banner is gone and so is the ADR-0087 disposition marker — both belonged to the narrowing that owned them. Something published still moves (two return types in the shipped.d.ts), soskip-changesetwould be wrong; a producer-side fix in a released package is exactly what apatchentry is for.Re-run locally against a
pull_requestpayload carrying this body, before pushing:GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD_PATH→ exit 0,NOT DECLARED — the clause-② declaration reads 'no'. The same command against the OLD body reproduces the CI red, so the local run is a measurement and not a hopeful one.ADR-0136 is renumbered to ADR-0137
PR #18480 added
docs/adr/0136-declared-journeys-as-priority-anchor.mdabout 42 hours earlier.scripts/check-adr-anchors.mjsprescribes exactly this — the NEW record takes the next free number, and renumbering an already-accepted record was ruled out, so before it is referenced is the only cheap moment.0137re-verified free on this rework, not inherited from the earlier sweep:git ls-tree origin/main docs/adr/maindocs/adr/*files across all 31 open PRs (GET /pulls/{n}/files)0136: #18480 and this PR. Zero on 0137The scan lit twice on
0136, so the zero on0137is a reading and not a dead query.Three corrections the record owed
1. Its Status line claimed an implementation it no longer has. It now says what is true: this record declares and implements nothing. D1's authoring refusal is batch #122 item 2's, carried by #18638; D2 / D3 / D4 are consumer-delivered in objectui#8069.
2. The gate-slot conversion is RULED and IN FLIGHT, not "filed as a follow-up". The dangling sentence is gone. The record's claim that converting the gate slots "would bake a direction the ruling did not give" is true of batch #119 and was silent about batch #122 item 2, which gave exactly that direction six days earlier, and about #18638 which implements it. The claim is now stated as what it is — a statement about which ruling authorizes what, not a reason the conversion should wait — and the lint-side cost (
validate-visibility-predicates.ts'scelRefusalrecords the opposite position today) is named as a cost #18638 carries, not as an objection.3. The hand enumeration is replaced by a citation of #15811's census, because the hand list had already rotted. It omitted
packages/spec/src/system/settings-manifest.zod.ts:424and:686, bothvisible: SettingsVisibilityInputSchema, which isExpressionInputSchema.superRefine(...). Measured throughSettingsManifestSchema.safeParseon the builtdist:visible:686):424){ dialect: 'cel', ast: … }{ dialect: 'cel', source: ' ' }' ''data.provider.toUpperCase()'custom@visiblecustom@specifiers.0.visible"data.provider === 'smtp'"The refinement is live at both slots and narrows neither the
ast-only nor the blank-sourcearm —if (!source) return;is the line, and the lit control is what makes the six ACCEPTEDs a reading.The ADR-0058 D7 roster entry — re-derived, then removed
The ruling's KEEP list names it, and carries NO re-derive clause. The instruction to re-derive came from this seat's dispatch brief, not from the maintainer — recorded here because the earlier wording attributed it to the ruling. The re-derivation concluded the entry no longer belongs (measured: discovery finds 37 positions with the line present and 37 without, floor 37 unchanged and met at 37; the alias types zero slots), so the line is removed here pending the maintainer's explicit confirmation of that removal.
The roster lists schemas that declare an expression surface — "a slot whose accepted grammar is narrower gets its own schema and must be listed here too".
PredicateInputSchemaearned its place only while the rebinding made it= EvaluatedExpressionInputSchemaand bound the triad to it from another file. Reverted, it is a plain alias ofExpressionInputSchematyping no slot, and the ledger header's limitation 2 names it as the standing latent example — leaving it rostered would make that paragraph false. #18638 measured the same thing independently: 「PredicateInputSchemais a plain alias ofExpressionInputSchemawith zero slot users; it stays wide with the schema it aliases」.Measured twice on this branch with the revert already applied, by raising the
headfloor to 9999 throughablation-replace.mjsso the assertion prints the count:Identical, because the three triad positions are head-matched by
ExpressionInputSchemaagain. Both mutations landed and both restores verified on disk (anchor 1 -> 0, thenblob == HEADandgit diff HEADempty). Identity grep agrees:PredicateInputSchemahas 2 hits underpackages/spec/src/**/*.zod.ts— its own definition and itsz.inputcompanion, zero slots — against a lit control of 19 files forExpressionInputSchemaand a dark control of 0.⛔ Not a gate weakening. The
headfloor stays 37 and is met at 37; no ledger row is deleted, no floor is lowered, no test is skipped or quarantined. The same three surfaces are discovered through the schema that types them. Both dogfood files are byte-identical to merged main. The same statement holds for the two other removals: the triad pin test and the ADR-0087 entry are removed because the behaviour they recorded is no longer in this PR — not to turn anything green.packages/qa/dogfoodre-run after the removal: 7 passed (7).⛔ GOVERNED SURFACE — this PR parks as a draft, by design
docs/adr/**is on the register, so this is the regime's correct resting state, not a stall. An authorized approval is owed before any seat lands this. This seat has not flipped it ready, has not enqueued it and has not armed auto-merge. Theneeds:contract-reviewcarrier is the review seat's and stays hung; a fresh at-tier review is owed on this head.Evidence
pnpm --filter @objectstack/spec check:generated— proved exactly 2 of 16 artifacts stale (api-surface-declarations/,content/docs/references/**) and--fixregenerated only those two. Re-run after: 16 of 16 up to date.check:migration-registrygreen with the entry file deleted — the generated regions matchentries/.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackand reconciled with--ran; results and every non-zero exit are in the report comment on spec/ADR-0089: a form field-rule predicate that faults refuses the submit loudly; visibility stays fail-open at render; a blank predicate is refused at authoring — fault semantics become part of the contract (objectui#8069 ruling A) #17778.origin/mainand 10 of the files the families are derived from moved in that range. Noorigin/mainmerge was taken in this rework, on purpose — this PR rebases after feat(spec)!: every engine-evaluated expression slot requires a non-blanksource#18638 lands, and a merge now would move themerge=os-regenartefacts for a base that is about to change. So the family list is this tree's, and CI on the merge queue's rebuilt generation is what covers the rest.Acceptance notes
packages/spec/src/data/field.zod.ts:1513expression(the formula slot) is an evaluated slot onExpressionInputSchema. Untouched, and no longer this PR's business at all: it is inside feat(spec)!: every engine-evaluated expression slot requires a non-blanksource#18638's 36-position census.PredicateSchema/PredicateInputSchemahave zero slot users — measured above. The ruling says they stay as they are and that a later card may retire them as dead symbols on their own measurement. No anchor is left claiming otherwise: the anchor this PR added forshared/expression.zod.tsis deleted, and thefield.zod.tsanchor is back to its pre-PR text.Consumersline still namesshared/expression.zod.tsanddata/field.zod.ts. That is the set of files the DECISION governs, which is unchanged; it is not a claim that this PR edits them for that reason.维护者速读(草稿)
改了什么 — 按 batch #160 item 1 的裁决 A,把这个 PR 里的协议收窄整段拿掉:字段三条规则槽位(
visibleWhen/readonlyWhen/requiredWhen)回到原样,Predicate*两个别名回到原样(仍然是宽的持久化契约),对应的 ADR-0087 迁移条目、pin 测试、两个 ADR anchor、以及只因它们才产生的 api-surface 与参考文档行,全部删除。留下的是:ADR-0137(改号后的 fault 语义记录)、ADR-0089 的指针附录、cel/expression的返回类型修复。另外按指示重新推导后,删掉了 ADR-0058 D7 的那一行 roster 条目。为什么改 — 同一个收窄早在六天前就被 batch #122 item 2 裁决过了,覆盖全部 36 个求值槽位(包含本卡的三条字段规则),并且由 PR #18638 用一个 ADR-0087 id 承载、先落地。两个 PR 各带一份收窄,就是一次迁移两个 id、两份 CHANGELOG 说法。#17778 裁决的是 fault 语义,不是承载它的符号,所以记录留下、收窄交出去,没有任何被裁决过的东西丢失。
风险与代价(含回滚) — 风险很低:协议行为零变化(没有任何 zod schema 移动),对外只剩两个函数返回类型收窄,而
EvaluatedExpression可赋值给Expression,所有调用点照常编译。changeset 因此从minor+ BREAKING 降为patch,Clause-②重判为no(三项读数在上表)。代价是本 PR 不再自带任何强制:D1 的编写期拒收要等 #18638;这一点在记录的 Status 和 Scope boundary 里明写了,不是留给读者去发现。回滚成本极低——本轮全部是删除与还原,恢复即 revert 这四个 commit。席位意见 — (留空)
你要做的 — 这个 PR 碰了
docs/adr/**,属受管面,停在 draft 等一个授权批准,这是制度的正常终态。需要你看的是三件事:① ADR-0137 现在只声明不实现——D1 交给 #18638、D2/D3/D4 交给 objectui#8069,这个归属你是否认可;② 记录里原来那句「gate 槽位转换会写进裁决没给的方向」已改写为「那只对 batch #119 成立;batch #122 item 2 给了这个方向,#18638 正在做」,这个更正你是否同意;③ D7 roster 那一行被删而不是保留——测量是:删与不删,head发现数都是 37(地板 37),且PredicateInputSchema零槽位使用,所以它不再是「更窄的别名」。裁决原文把它列在「保留」里,且未附任何重新推导的条款 —— 要求重新推导的是本席派发令,不是维护者(先前措辞把它归给了裁决,此处更正)。推导结论是该条目不再属于花名册(实测:该行在与不在,发现数都是 37,地板 37 未动且 met at 37;该别名零槽位),故此处删除,等您明确确认这一删除。Generated by Claude Code